feat(multisig-client): offline switch-guardian proposal creation (#433) - #436
Conversation
Add createSwitchGuardianProposalOffline: build, sign, and cache a switch-guardian proposal without contacting the current GUARDIAN, so an account can leave an unreachable operator (mirrors the Rust create_proposal_offline, including its pre-build network sync). The returned ExportedProposal carries the proposer's signature and feeds the existing importProposal / signProposalOffline / executeProposal trio. The build step is shared with createSwitchGuardianProposal via buildSwitchGuardianSummary so the online and offline proposals for the same operation cannot drift, and the offline method reuses importProposal + signProposalOffline for caching and signing instead of re-implementing them. Also export computeCommitmentFromTxSummary for hand-rolled export/import flows, now returning normalized hex directly comparable to proposal ids.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: Comment |
zeljkoX
left a comment
There was a problem hiding this comment.
LGTM
Added one minor commment.
| const webClient = await this.getRawClient(); | ||
| await retryRpcRead(() => webClient.syncState(), this.rpcConfig); |
There was a problem hiding this comment.
Rust’s sync_network_only() also refreshes the cached account after syncing. Here, webClient.syncState() updates the WASM store but leaves this.account, this.threshold, this.signerCommitments, and this.procedureThresholds at their previous values.
That means the summary can be built against the latest account state while readiness and signature validation use stale configuration. For example, after the on-chain threshold changes from 1 to 2, this client could incorrectly consider the proposal ready with one signature and fail only during submission.
Please reload the account from the store and call refreshConfigFromAccount() after this sync, ideally through a private syncNetworkOnly() helper shared with the post-switch sync in executeProposal.
…sync (#436 review) The offline create synced the WASM store but left this.account and the cached threshold/signer/procedure config stale, so readiness and requiredSignatures could be computed from outdated state (e.g. a threshold raised on-chain to 2 would still mark the proposal ready with one signature, failing only at submission). Extract a private syncNetworkOnly() that syncs and then reloads the account via refreshConfigFromAccount — full parity with the Rust sync_network_only — and share it with executeProposal's post-switch sync. Also address the CodeQL note on the test fetch stub by matching the parsed URL origin instead of a substring prefix.
Closes #433.
Adds
createSwitchGuardianProposalOffline(newGuardianEndpoint, newGuardianPubkey, { nonce }?)to the TS client — the creation counterpart to the existingimportProposal/signProposalOffline/executeProposaltrio, mirroring the Rustcreate_proposal_offline. Nothing is pushed to the current GUARDIAN, so an account can leave an unreachable operator (the 0xMiden/wallet#782 scenario): the method syncs with the Miden node (Rustsync_network_onlyparity), verifies the new endpoint's/pubkeycommitment, builds and signs the summary locally, caches the proposal, and returns theExportedProposal(proposer signature included) for side-channel cosigning.Design notes:
buildSwitchGuardianSummaryshared withcreateSwitchGuardianProposal, so the online and offline proposals for the same operation cannot drift.importProposal+signProposalOfflineend to end rather than re-implementing them, so the local copy carries exactly the guarantees a cosigner-imported one does.computeCommitmentFromTxSummaryis now exported (second ask in the issue) and returns normalized hex, directly comparable toExportedProposal.commitment/Proposal.id.